/** * WorthDoing.ai * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: src/app/api/opportunities/[id]/route.ts * Description: Full opportunity report data — report, scores, evidence with sources, competitors, skeptic case. */ import { NextRequest, NextResponse } from "next/server"; import { and, asc, eq, inArray } from "drizzle-orm"; import { z } from "zod"; import { db } from "@/lib/db/client"; import { opportunities, opportunityScores, opportunityEvidence, opportunityCompetitors, competitors, evidence, sources, investigations, hypotheses, } from "@/lib/db/schema"; export const dynamic = "force-dynamic"; export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) { const { id } = await ctx.params; if (!z.string().uuid().safeParse(id).success) { return NextResponse.json({ error: "Invalid opportunity id." }, { status: 400 }); } const [opp] = await db.select().from(opportunities).where(eq(opportunities.id, id)); if (!opp) return NextResponse.json({ error: "Opportunity not found." }, { status: 404 }); const [inv] = await db.select().from(investigations).where(eq(investigations.id, opp.investigationId)); const hyp = opp.hypothesisId ? (await db.select().from(hypotheses).where(eq(hypotheses.id, opp.hypothesisId)))[0] : null; const scores = await db.select().from(opportunityScores).where(eq(opportunityScores.opportunityId, id)); const links = await db.select().from(opportunityEvidence).where(eq(opportunityEvidence.opportunityId, id)); const evidenceIds = links.map((l) => l.evidenceId); const evRows = evidenceIds.length ? await db .select({ id: evidence.id, kind: evidence.kind, quote: evidence.quote, summary: evidence.summary, strength: evidence.strength, createdAt: evidence.createdAt, sourceUrl: sources.canonicalUrl, sourceTitle: sources.title, sourceDomain: sources.domain, }) .from(evidence) .innerJoin(sources, eq(sources.id, evidence.sourceId)) .where(and(eq(evidence.investigationId, opp.investigationId), inArray(evidence.id, evidenceIds))) .orderBy(asc(evidence.createdAt)) : []; const roleByEvidence = new Map(links.map((l) => [l.evidenceId, l.role])); const comps = await db .select({ name: competitors.name, url: competitors.url, description: competitors.description, note: opportunityCompetitors.note, }) .from(opportunityCompetitors) .innerJoin(competitors, eq(competitors.id, opportunityCompetitors.competitorId)) .where(eq(opportunityCompetitors.opportunityId, id)); return NextResponse.json({ opportunity: { id: opp.id, title: opp.title, summary: opp.summary, problem: opp.problem, whyNow: opp.whyNow, risks: opp.risks, skepticCase: opp.skepticCase, reportMd: opp.reportMd, status: opp.status, worthScore: opp.worthScore, evidenceConfidence: opp.evidenceConfidence, createdAt: opp.createdAt, }, investigation: inv ? { id: inv.id, objective: inv.objective, status: inv.status, completedAt: inv.completedAt } : null, hypothesis: hyp ? { id: hyp.id, title: hyp.title, statement: hyp.statement, status: hyp.status, confidence: hyp.confidence } : null, scores, evidence: evRows.map((e, i) => ({ ...e, index: i + 1, role: roleByEvidence.get(e.id) ?? "context" })), competitors: comps, }); }